/**************************************************************************\
    Copyright Microsoft Corporation. All Rights Reserved.
\**************************************************************************/

namespace C4F.DevKit.Contacts
{
    using C4F.DevKit.Contacts.Interop;
    using C4F.DevKit.Contacts.Standard;
    using System;
    using System.Collections.Generic;
    using System.ComponentModel;
    using System.IO;
    using System.Runtime.InteropServices;
    using System.Text;
    using System.Threading;
    using System.Windows.Threading;

    // Disambiguate from Contact.Path property.
    using IOPath = System.IO.Path;
    using System.Diagnostics.CodeAnalysis;

    /// <summary>
    /// Options for Contact.CommitChanges.  These can be ORed together to combine options.
    /// </summary>
    [Flags]
    public enum ContactCommitOptions
    {
        /// <summary>
        /// Default options.
        /// </summary>
        None = 0,

        /// <summary>
        /// The backing file is renamed to match the formatted name of the Contact.
        /// </summary>
        /// <remarks>
        /// Note that this flag has no effect if the file name isn't the same as the starting formatted name.
        /// If multiple FormattedNames are present, the one labeled Preferred is preferred.
        /// </remarks>
        SyncStorageWithFormattedName = 1,

        /// <summary>
        /// The backing file is renamed to match the formatted name of the Contact.
        /// </summary>
        /// <remarks>
        /// Note that this flag overwrites any user-made changes to the file name.  Also this flag contains
        /// SyncStorageWithFormattedName.
        /// </remarks>
        ForceSyncStorageWithFormattedName = (1 << 1) | SyncStorageWithFormattedName,

        /// <summary>
        /// Changes made to the storage since this contact instance was loaded will be overwritten by these changes.
        /// </summary>
        /// <remarks>
        /// Without this flag, an IncompatibleChangesException will be thrown if the storage file isn't the same
        /// at the time of the CommitChanges call as it was when this contact was loaded.
        /// </remarks>
        IgnoreChangeConflicts = 1 << 2,
    }

    /// <summary>
    /// Flagged enumeration for the different high-level types a contact can represent.
    /// </summary>
    /// <remarks>
    /// The values can be bitwise combined for APIs that filter based on a contact's type,
    /// such as GetContactCollection(ContactType) on ContactManager.
    /// </remarks>
    [Flags]
    public enum ContactType
    {
        /// <summary>
        /// The contact type is unknown.
        /// </summary>
        /// <remarks>
        /// This has the sentinel value of 0.
        /// </remarks>
        Unknown  = 0,

        /// <summary>
        /// The contact represents a person.
        /// </summary>
        Contact  = 1 << 0,
        
        /// <summary>
        /// The contact represents a mixed collection of other contacts.
        /// </summary>
        Group    = 1 << 1,

        /// <summary>
        /// The contact represents a business or organization.
        /// </summary>
        Business = 1 << 2,

        /// <summary>
        /// Bitwise combination of Contact, Business, and Group.
        /// </summary>
        All = Contact | Group | Business,
    }

    /// <summary>
    /// Get and set properties for a single contact.
    /// </summary>
    public class Contact : DispatcherObject, IDisposable, INotifyPropertyChanged
    {
        #region Public Static Methods

        /// <summary>
        /// Save a contact to a VCard file.
        /// </summary>
        /// <param name="contact">The contact to convert to a vCard.</param>
        /// <param name="filePath">
        /// The path where the vCard should be saved.
        /// If it doesn't exist it will be created.
        /// If it does exist it will be overridden.
        /// </param>
        /// <remarks>
        /// This takes a subset of the possible properties in the contact and writes
        /// them to a file formatted to the vCard 2.1 specification.
        /// The copy is not full fidelity.  It also does not support writing string
        /// properties that are outside the range of ANSI values.<para/>
        /// The vCard specification is available at http://www.imc.org/pdi/pdiproddev.html.
        /// </remarks>
        // TODO: Should provide an overload to take a collection of Contacts.
        public static void SaveToVCard21(Contact contact, string filePath)
        {
            Validate.NotNull(contact, "contact");
            VCard21.EncodeToVCard(contact, filePath);
        }

        /// <summary>
        /// Parse a vCard file for its contacts.
        /// </summary>
        /// <param name="vcardStream">
        /// The text content to parse.  It should be in a format consistent with the vCard 2.1
        /// specification.  It can contain multiple vCard objects, either cascaded or embedded.
        /// </param>
        /// <returns>
        /// The collection of vCard objects contained in the stream viewed as Contacts.
        /// If the stream doesn't contain any valid vCard objects an empty collection is
        /// returned.  If the stream contains improperly formatted vCard object data then
        /// an exception is thrown.
        /// </returns>
        /// <remarks>
        /// Officially this supports files formatted as specified by the vCard 2.1
        /// specification.  It may read contacts that are in vCard 3.0 format but only
        /// insomuch as 3.0 is a superset of 2.1.<para/>
        /// The stream does not need to be in ANSI character encoding.
        /// If the encoding is of an extended character set then those characters can be
        /// written into the contact without data loss.  If the stream changes character
        /// encoding on a per-property basis (which is supported by the vCard spec) and
        /// the TextReader doesn't properly step through these characters then the results
        /// of this function are undefined (likely it will throw an exception).<para/>
        /// Groupings of properties as described in the vCard specification are ignored.
        /// The CHARSET attribute on properties is also ignored, as the stream is treated
        /// as text.<para/>
        /// The vCard specification is available at http://www.imc.org/pdi/pdiproddev.html.
        /// </remarks>
        public static ICollection<Contact> CreateFromVCard(TextReader vcardStream)
        {
            Validate.NotNull(vcardStream, "vcardStream");
            return VCard21.ReadVCard(vcardStream);
        }
        #endregion

        #region Statics
        private const string _defaultFileName = "Unknown";
        private const string _stockInvalidUrl = "http://invalid_url_in_contact";
        private const string _exceptionStringBadThreadId = "Contacts can only be created on STA threads and can only be accessed from the thread on which they were created.";

        private static readonly Random _randomNumberGenerator = new Random();
        private static readonly Dictionary<string, ContactType> _extensionMap = new Dictionary<string, ContactType>(StringComparer.InvariantCultureIgnoreCase);
        private static readonly Dictionary<ContactType, string> _typeMap = new Dictionary<ContactType, string>();

        /// <summary>
        /// Infer the type of a contact based on a file path.
        /// </summary>
        /// <param name="path">
        /// The path of the file to determine the ContactType for.
        /// It can be a relative or absolute path.
        /// </param>
        /// <returns>
        /// A ContactType based on the file extension of the given path.
        /// If the path doesn't have an extension or the extension doesn't
        /// map to a known ContactType, Unknown is returned.
        /// </returns>
        public static ContactType GetTypeFromExtension(string path)
        {
            Validate.NeitherNullNorEmpty(path, "path");

            if (_extensionMap.Count == 0)
            {
                // Populate this on demand.
                _extensionMap.Add(".contact", ContactType.Contact);
                _extensionMap.Add(".business", ContactType.Business);
                _extensionMap.Add(".group", ContactType.Group);
            }

            ContactType ct;
            path = IOPath.GetExtension(path);
            Assert.IsNotNull(path);
            if (!_extensionMap.TryGetValue(IOPath.GetExtension(path), out ct))
            {
                ct = ContactType.Unknown;
            }
            return ct;
        }

        /// <summary>
        /// Get the file extensions associated with given ContactTypes.
        /// </summary>
        /// <param name="type">The ContactTypes to get the extensions for.
        /// This can be a bitwise combination of multiple ContactTypes.</param>
        /// <returns>
        /// Returns the file extensions associated with the given ContactType.
        /// If more than one type is contained in the parameter, the multiple
        /// extensions are returned in a string split by the pipe character ('|').
        /// </returns>
        /// <exception cref="System.ArgumentException">
        /// If the given type is ContactType.Unknown or contains bits outside the valid
        /// range of ContactType.All.
        /// </exception>
        public static string GetExtensionsFromType(ContactType type)
        {
            return _GetExtensionsFromType(type, true);
        }

        #endregion

        #region Fields
        private IContact _nativeContact;
        private ContactManager _manager;
        private string _path;
        private string _id;
        private string _originalName;
        private ILabeledPropertyCollection<Name> _nameCollection;
        private ILabeledPropertyCollection<Position> _positionCollection;
        private ILabeledPropertyCollection<Guid?> _contactIdCollection;
        private ILabeledPropertyCollection<DateTime?> _dateCollection;
        private ILabeledPropertyCollection<Uri> _urlCollection;
        private ILabeledPropertyCollection<IMAddress> _imCollection;
        private ILabeledPropertyCollection<EmailAddress> _emailCollection;
        private ILabeledPropertyCollection<Person> _personCollection;
        private ILabeledPropertyCollection<Photo> _photoCollection;
        private ILabeledPropertyCollection<Certificate> _certCollection;
        private ILabeledPropertyCollection<PhysicalAddress> _addressCollection;
        private ILabeledPropertyCollection<PhoneNumber> _numberCollection;
        private bool _isStateValid = true;
        private ContactType _type = ContactType.Unknown;
        #endregion

        #region Internal Static Utilities shared with ContactManager
        internal static void CopyToDirectory(Contact contact, string directory)
        {
            Assert.IsNotNull(contact);
            Assert.IsFalse(string.IsNullOrEmpty(directory));

            FileStream fstream = null;
            try
            {
                _GenerateUniqueFileName(
                    contact,
                    _GetExtensionsFromType(contact.ContactType, false),
                    directory,
                    delegate(string newPath, bool willRetry)
                    {
                        try
                        {
                            fstream = new FileStream(newPath, FileMode.CreateNew);
                            return true;
                        }
                        // This is expected in the case that the file already exists.
                        catch (IOException)
                        {
                            if (willRetry)
                            {
                                return false;
                            }
                            throw;
                        }
                    });
                using (ManagedIStream istream = new ManagedIStream(fstream))
                {
                    ((IPersistStream)contact._nativeContact).Save(istream, true);
                }
                fstream.Close();
                fstream = null;
            }
            finally
            {
                Utility.SafeDispose(ref fstream);
            }
        }
        #endregion

        #region Private Utilities
        // delegate for _GenerateUniqueFileName
        private delegate bool _UniqueFileOperation(string path, bool willRetry);

        private static string _GenerateUniqueFileName(Contact contact, string extension, string directory, _UniqueFileOperation fileOp)
        {
            Validate.NotNull(contact, "contact");
            Validate.NotNull(directory, "directory");

            // Because of race conditions in the environment it's possible that the directory doesn't exist
            // or gets deleted at any point in this call.  Let the delegate deal with it as it may.

            string displayName = contact.Names.Default.FormattedName;
            if (string.IsNullOrEmpty(displayName))
            {
                displayName = _defaultFileName;
            }

            // Replace illegal file characters with underscores.
            displayName = _MakeValidFileName(displayName);

            string returnPath = null;
            for (int i = 0; i <= 50; ++i)
            {
                if (0 == i)
                {
                    returnPath = IOPath.Combine(directory, displayName) + extension;
                    if (fileOp(returnPath, true))
                    {
                        return returnPath;
                    }
                }
                else if (40 >= i)
                {
                    returnPath = IOPath.Combine(directory, displayName) + " (" + i.ToString((IFormatProvider)null) + ")" + extension;
                    if (fileOp(returnPath, true))
                    {
                        return returnPath;
                    }
                }
                else
                {
                    // At this point we're hitting pathological cases.  This should stir things up enough that it works.
                    // If this fails because of naming conflicts after an extra 10 tries, then I don't care.
                    returnPath = IOPath.Combine(directory, displayName) + " (" + _randomNumberGenerator.Next() + ")" + extension;
                    if (fileOp(returnPath, i != 50))
                    {
                        return returnPath;
                    }
                }
            }

            // If this went through the full loop without being able to successfully perform the operation,
            // then we need to throw an exception.  The willRetry parameter allows the caller to give more
            // specific exceptions.
            Assert.Fail("Internal only function.  The passed in delegate should have thrown before getting here.");
            throw new IOException();
        }

        private static string _GetExtensionsFromType(ContactType type, bool allowMultiset)
        {
            // Verify that the type doesn't contain bits outside the valid range and it contains
            // at least one bit inside the valid range.
            if ((ContactType.Unknown != (type & ~ContactType.All))
                || (ContactType.Unknown == (type & ContactType.All)))
            {
                throw new ArgumentException("The provided ContactType is not within the valid range.", "type"); 
            }

            if (_typeMap.Count == 0)
            {
                _typeMap.Add(ContactType.Business, ".business");
                _typeMap.Add(ContactType.Contact, ".contact");
                _typeMap.Add(ContactType.Group, ".group");
            }

            string ext;
            if (!_typeMap.TryGetValue(type, out ext) && allowMultiset)
            {
                ext = null;
                // Not a single value, check if we should try to compose a set.
                if ((type & ContactType.All) != 0)
                {
                    StringBuilder extBuilder = new StringBuilder();
                    foreach (ContactType key in _typeMap.Keys)
                    {
                        if ((type & key) != 0)
                        {
                            extBuilder.Append(_typeMap[key]).Append("|");
                        }
                    }
                    // If nothing is returned then it's bad args.
                    if (0 != extBuilder.Length)
                    {
                        ext = extBuilder.ToString(0, extBuilder.Length - 1);
                    }
                }
            }

            // Checked this above.  The type should have yielded a result.
            Assert.Implies(allowMultiset, null != ext);

            return ext;
        }

        private static bool _IsValidSingleContactType(ContactType type, bool isUnknownOK)
        {
            return type == ContactType.Contact
                || type == ContactType.Business
                || type == ContactType.Group
                || (isUnknownOK
                    ? (type == ContactType.Unknown)
                    : false);
        }

        private static string _MakeValidFileName(string invalidPath)
        {
            return invalidPath
                .Replace('\\', '_')
                .Replace('/', '_')
                .Replace(':', '_')
                .Replace('*', '_')
                .Replace('?', '_')
                .Replace('\"', '_')
                .Replace('<', '_')
                .Replace('>', '_')
                .Replace('|', '_');
        }

        /// <summary>
        /// Ensure that this contact is in a valid state.
        /// </summary>
        /// <exception cref="ObjectDisposedException"></exception>
        /// <exception cref="InvalidStateException"></exception>
        private void _Validate()
        {
            // DispatcherObject baseclass enforces that this object
            // is only accessed from the thread on which it was created.
            this.VerifyAccess();
            // All the constructors should catch this also.
            Assert.IsApartmentState(ApartmentState.STA);

            if (null == _nativeContact)
            {
                throw new ObjectDisposedException("this");
            }

            if (!_isStateValid)
            {
                throw new InvalidStateException("The contact is in an inconsistent state because of partial updates.  It needs to be reloaded to continue.  Path and Id are the only accessible methods at this point.");
            }
        }

        /// <summary>
        /// Wraps SetBinaryProperty and RemoveProperty so the caller doesn't need to check whether the value is null.
        /// </summary>
        /// <param name="propertyName">
        /// The property being set or removed.  It must not be null.
        /// </param>
        /// <param name="value">
        /// The value to set for propertyName.  If it is null the property is removed.
        /// </param>
        /// <param name="valueType">
        /// The mime type of the value stream.  If it is null a default is provided.
        /// </param>
        private void _SetOrRemoveBinaryProperty(string propertyName, Stream value, string valueType)
        {
            Assert.IsNotNull(propertyName);
            if (null == value)
            {
                RemoveProperty(propertyName);
            }
            else
            {
                SetBinaryProperty(propertyName, value, valueType);
            }
        }

        /// <summary>
        /// Wraps SetDateProperty and RemoveProperty so the caller doesn't need to check whether the value is null.
        /// </summary>
        /// <param name="propertyName">
        /// The property being set or removed.  It must not be null.
        /// </param>
        /// <param name="value">
        /// The value to set for propertyName.  If it is null the property is removed.
        /// </param>
        private void _SetOrRemoveDateProperty(string propertyName, DateTime? value)
        {
            Assert.IsNotNull(propertyName);
            if (value.HasValue)
            {
                SetDateProperty(propertyName, value.Value);
            }
            else
            {
                RemoveProperty(propertyName);
            }
        }

        /// <summary>
        /// Wraps SetStringProperty and RemoveProperty so the caller doesn't need to check whether the value is null.
        /// </summary>
        /// <param name="propertyName">
        /// The property being set or removed.  It must not be null.
        /// </param>
        /// <param name="value">
        /// The value to set for propertyName.  If it is null the property is removed.
        /// </param>
        private void _SetOrRemoveStringProperty(string propertyName, string value)
        {
            Assert.IsNotNull(propertyName);
            if (string.IsNullOrEmpty(value))
            {
                RemoveProperty(propertyName);
            }
            else
            {
                SetStringProperty(propertyName, value);
            }
        }

        /// <summary>
        /// Renames the file that the contact is backed by based on the preferred FormattedName property.
        /// </summary>
        /// <remarks>
        /// Note that this reloads the contact from the file, so it's possible that the contact won't be available.
        /// If that's the case then this object is in an invalid state and _Validate will generally throw.
        /// </remarks>
        private void _RenameContact()
        {
            Assert.IsNotNull(_nativeContact);
            Assert.IsNotNull(Path);

            // This could be done by saving to the new file name, but it's better if NTFS object Ids can still be tracked.
            string newPath = _GenerateUniqueFileName(
                this,
                IOPath.GetExtension(Path),
                IOPath.GetDirectoryName(Path),
                delegate(string tryPath, bool willRetry)
                {
                    try
                    {
                        File.Move(Path, tryPath);
                        return true;
                    }
                    catch (IOException)
                    {
                        if (willRetry)
                        {
                            // This gets thrown if the file couldn't be moved because the file already exists.
                            return false;
                        }
                        // If _GenerateUniqueFileName isn't going to retry this delegate, propagate the error.
                        throw;
                    }
                });

            // When dealing with the file system like this we can't recover from errors.
            // This shouldn't be called inside other functions that put the contact in a temporary invalid state.
            Assert.IsTrue(_isStateValid);
            _isStateValid = false;

            // Reload the contact.

            // Pre-cache this in case of failures.  It will get reloaded as part of this call.
            _path = newPath;
            _ReloadContactFromFile(newPath);

            // Done! :)
            _isStateValid = true;

            // Notify listeners that the path has changed.
            NotifyPropertyChanged(this, new ContactPropertyChangedEventArgs("Path", ContactPropertyChangeType.PathChanged));
        }

        /// <summary>
        /// Commits changes for a new ContactManager associated contact.
        /// </summary>
        /// <remarks>
        /// This generates file storage for the contact, and will reload it from the manager.
        /// </remarks>
        private void _CommitAndUpdateNewContact()
        {
            Assert.IsNotNull(_manager);
            Assert.IsNull(_path);

            // Only do this for new contacts (where type should already have been verified).
            // Otherwise we just keep the pre-existing extension, whatever it may be.
            string extension = _GetExtensionsFromType(_type, false);
            Assert.IsNotNull(extension);

            FileStream fstream = null;
            try
            {
                // Cache this early in case we go to an invalid state.
                // Reload it from the contact later to ensure that the format is the same.
                _path = _GenerateUniqueFileName(
                    this,
                    extension,
                    _manager.RootDirectory,
                    delegate(string newPath, bool willRetry)
                    {
                        try
                        {
                            fstream = new FileStream(newPath, FileMode.CreateNew);
                            return true;
                        }
                        // This is expected in the case that the file already exists.
                        catch (IOException)
                        {
                            if (willRetry)
                            {
                                return false;
                            }
                            throw;
                        }
                    });

                // Hereafter things need to keep on SUCCEEDing.
                // This shouldn't be called inside other functions that put the contact in a temporary invalid state.
                Assert.IsTrue(_isStateValid);
                _isStateValid = false;
                using (ManagedIStream istream = new ManagedIStream(fstream))
                {
                    ((IPersistStream)_nativeContact).Save(istream, true);
                }
                fstream.Close();

                // Generate a contactId for this contact that includes the path.
                // It causes the ContactManager to explicitly load the correct contact (quickly).
                Assert.IsTrue(Id.Contains("/GUID:"));
                Assert.IsFalse(Id.Contains("/PATH"));
                // Reload this contact from the file since it's been renamed.
                // Subsequent Saves should be able to check for conflicting changes and such.
                _ReloadContactFromFile(fstream.Name);

                // Done! :)
                _isStateValid = true;

                // Notify listeners that there's now a path.
                NotifyPropertyChanged(this, new ContactPropertyChangedEventArgs("Path", ContactPropertyChangeType.PathChanged));
            }
            finally
            {
                Utility.SafeDispose(ref fstream);
            }
        }

        /// <summary>
        /// Reload the backing native contact from a file path.
        /// This is called in the context that the file that backs the contact is new or has been moved.
        /// </summary>
        /// <param name="path">
        /// The path to use for reloading.
        /// </param>
        private void _ReloadContactFromFile(string path)
        {
            ContactRcw tempContact = null;
            Utility.SafeRelease(ref _nativeContact);

            try
            {
                tempContact = new ContactRcw();
                tempContact.Load(path, STGM.DIRECT)
                    .ThrowIfFailed("Error loading the contact from the file.");

                string id;
                // I know this loaded from a file, and it has an Id.  These two calls need to succeed.
                ContactUtil.GetID(tempContact, out id)
                    .ThrowIfFailed("Error verifying the ID of the contact.  The contact is in an invalid state and must be reloaded to continue.");

                string localPath;
                ContactUtil.GetPath(tempContact, out localPath)
                    .ThrowIfFailed("Error verifying the path of the contact.  The contact is in an invalid state and must be reloaded to continue.");

                _nativeContact = tempContact;
                tempContact = null;
                _id = id;
                _path = localPath;
            }
            finally
            {
                Utility.SafeRelease(ref tempContact);
            }
        }

        #endregion

        #region Internal Utilities

        /// <summary>
        /// A kill switch for internal classes to invalidate this contact because of an incomplete operation
        /// </summary>
        internal void Invalidate()
        {
            _isStateValid = false;
        }

        internal void NotifyPropertyChanged(object sender, ContactPropertyChangedEventArgs e)
        {
            Assert.IsNotNull(sender);
            Assert.IsNotNull(e);

            if (null != PropertyChanged)
            {
                PropertyChanged(sender, e);
            }
        }

        /// <summary>
        /// Get the backing IContact instance.  Use this with caution!
        /// </summary>
        internal IContact IContact
        {
            get
            {
                _Validate();
                return _nativeContact;
            }
        }

        #endregion

        #region Constructors
        /// <summary>
        /// Create a new blank contact of type Contact.
        /// </summary>
        public Contact()
            : this((ContactManager)null, ContactType.Contact)
        {
        }

        /// <summary>
        /// Internal only constructor for ContactManager.
        /// </summary>
        /// <param name="manager">The associated ContactManager instance.</param>
        /// <param name="type">The type of the contact to create.</param>
        /// <remarks>
        /// This allows the contacts returned by IContactCollection to be managed by this class.
        /// The manager is associated with the contact, which allows for Save to be called without
        /// the contact being initially backed by a path.
        /// </remarks>
        internal Contact(ContactManager manager, ContactType type)
        {
            Validate.IsApartmentState(ApartmentState.STA, _exceptionStringBadThreadId);
            if (!_IsValidSingleContactType(type, false))
            {
                throw new ArgumentException("The provided type must be of a legal single value (also not ContactType.Unknown).", "type");
            }

            _manager = manager;

            _nativeContact = new ContactRcw();
            // Initialize the contact.
            IPersistStreamInit persistStreamInit = (IPersistStreamInit)_nativeContact;
            persistStreamInit.InitNew();
            
            ContactUtil.GetID(_nativeContact, out _id)
                .ThrowIfFailed("Error verifying the ID of the contact.");

            _originalName = string.Empty;
            _type = type;
        }

        /// <summary>
        /// Load a contact from a file.
        /// </summary>
        /// <param name="fileName">The file that contains the contact data to load.</param>
        /// <exception cref="InvalidDataException">
        /// The specified file exists but doesn't doesn't represent a valid contact.
        /// </exception>
        /// <exception cref="FileNotFoundException">
        /// The specified file couldn't be found.
        /// </exception>
        /// <exception cref="UnauthorizedAccessException">
        /// The specified file couldn't be opened for an unknown reason.  It may be that it's
        /// opened within incompatible sharing permissions.  Retrying the operation at a later
        /// time may succeed.
        /// </exception>
        public Contact(string fileName)
        {
            Validate.NotNull(fileName, "fileName");
            Validate.IsApartmentState(ApartmentState.STA, _exceptionStringBadThreadId);

            HRESULT hr = HRESULT.S_OK;

            // make the path absolute.
            fileName = IOPath.GetFullPath(fileName);

            _type = GetTypeFromExtension(fileName);

            _nativeContact = new ContactRcw();

            IPersistFile persistFile = (IPersistFile)_nativeContact;
            hr = persistFile.Load(fileName, STGM.DIRECT);
            if (hr.Failed())
            {
                if (HRESULT.WC_E_SYNTAX == hr || HRESULT.WC_E_GREATERTHAN == hr || (HRESULT)Win32Error.ERROR_INVALID_DATATYPE == hr)
                {
                    throw new InvalidDataException("The file doesn't contain a valid contact.");
                }
                if ((HRESULT)Win32Error.ERROR_FILE_NOT_FOUND == hr || (HRESULT)Win32Error.ERROR_PATH_NOT_FOUND == hr)
                {
                    Assert.IsFalse(File.Exists(fileName));
                    throw new FileNotFoundException();
                }
                if ((HRESULT)Win32Error.ERROR_SHARING_VIOLATION == hr)
                {
                    throw new UnauthorizedAccessException("The specified file, \"" + fileName + "\",  cannot currently be opened.  Trying again may succeed.");
                }
                hr.ThrowIfFailed("Error loading the contact from the file");
            }

            ContactUtil.GetPath(_nativeContact, out _path)
                .ThrowIfFailed("Error verifying the path of the contact.");

            ContactUtil.GetID(_nativeContact, out _id)
                .ThrowIfFailed("Error verifying the ID of the contact.");

            _originalName = Names.Default.FormattedName;
        }

        /// <summary>
        /// Load a contact from a stream.
        /// </summary>
        /// <param name="stream">The stream with the Contact contents to load.</param>
        /// <param name="type">
        /// The type of the contact to create.  ContactType.Unknown is valid for this constructor.
        /// </param>
        /// <remarks>
        /// This is the only Contact constructor where ContactType.Unknown is a valid type parameter.
        /// </remarks>
        public Contact(Stream stream, ContactType type)
        {
            Validate.NotNull(stream, "stream");
            Validate.IsApartmentState(ApartmentState.STA, _exceptionStringBadThreadId);
            if (!_IsValidSingleContactType(type, true))
            {
                throw new ArgumentException("ContactType must be a valid single value for this constructor (ContactType.Unknown is OK).", "type");
            }

            _type = type;

            _nativeContact = new ContactRcw();

            IPersistStream persistStream = (IPersistStream)_nativeContact;
            stream.Seek(0, SeekOrigin.Begin);
            using (ManagedIStream istm = new ManagedIStream(stream))
            {
                HRESULT hr = persistStream.Load(istm);
                if (HRESULT.WC_E_SYNTAX == hr || (HRESULT)Win32Error.ERROR_INVALID_DATATYPE == hr)
                {
                    throw new InvalidDataException("The stream is of an invalid format");
                }
                hr.ThrowIfFailed("An error occurred loading the contact");
            }

            // Loading from a stream so there is no path.

            ContactUtil.GetID(_nativeContact, out _id)
                .ThrowIfFailed("Error verifying the ID of the contact.");

            _originalName = Names.Default.FormattedName;
        }
        #endregion

        #region Public Properties and Methods

        public string AddNode(string collectionName, bool appendNode)
        {
            _Validate();
            Validate.NotNull(collectionName, "collectionName");

            string nodeName;
            ContactUtil.CreateArrayNode((IContactProperties)_nativeContact, collectionName, appendNode, out nodeName)
                .ThrowIfFailed("Unable to create a new array arrayNode with "  + collectionName + ".");

            // Notify listeners of a new node.
            NotifyPropertyChanged(this, new ContactPropertyChangedEventArgs(nodeName, appendNode ? ContactPropertyChangeType.NodeAppended : ContactPropertyChangeType.NodePrepended));

            return nodeName;
        }

        public void CommitChanges()
        {
            ContactCommitOptions flags = ContactCommitOptions.None;

            // There are different default flags if there's an associated manager
            if (null != _manager)
            {
                flags = ContactCommitOptions.SyncStorageWithFormattedName;
            }

            CommitChanges(flags);
        }

        public void CommitChanges(ContactCommitOptions flags)
        {
            _Validate();

            string formattedName = Names.Default.FormattedName;
            if (string.IsNullOrEmpty(formattedName))
            {
                formattedName = _defaultFileName;
            }

            if (null == _path)
            {
                if (null == _manager)
                {
                    throw new FileNotFoundException("The contact isn't backed by any store and isn't associated with a ContactManager.  To save it, use the SaveToFile method.");
                }

                _CommitAndUpdateNewContact();
            }
            else
            {
                HRESULT hr = ContactUtil.CommitContact(_nativeContact, 0 != (flags & ContactCommitOptions.IgnoreChangeConflicts));
                if ((HRESULT)Win32Error.ERROR_NESTING_NOT_ALLOWED == hr)
                {
                    throw new IncompatibleChangesException("Changes were made to this contact since it was loaded.  You can reload the contact to merge the changes, or you can call save with the Force flag.");
                }
                if ((HRESULT)Win32Error.ERROR_FILE_NOT_FOUND == hr)
                {
                    throw new IncompatibleChangesException("The underlying file backing this contact has been deleted.  You can call CommitChanges with the Force flag to replace it, or explicitly calls SaveToFile.");
                }
                hr.ThrowIfFailed("Error occurred trying to commit the changes to the contact.");

                // The Sync flag also contains the Force version of it.
                if (0 != (flags & ContactCommitOptions.SyncStorageWithFormattedName))
                {
                    // There are a couple ways this can be eligible for rename..
                    bool renameable = false;

                    // If force and file name doesn't equal FormattedName
                    if (!IOPath.GetFileName(Path).StartsWith(formattedName)
                        && (0 != (flags & ContactCommitOptions.SyncStorageWithFormattedName)))
                    {
                        renameable = true;
                    }

                    // If not force but original name equals file name, and original name doesn't equal Formatted name
                    if (IOPath.GetFileName(Path).StartsWith(_MakeValidFileName(_originalName))
                        && !string.Equals(_originalName, formattedName))
                    {
                        renameable = true;
                    }

                    if (renameable)
                    {
                        _RenameContact();
                    }
                }
            }

            // Update the original name for the next Save.
            _originalName = formattedName;
        }

        public string Id
        {
            get
            {
                // Don't call Validate.  Allow callers to still retrieve the Id when the state is inconsistent.

                string debugId;
                // If the native contact is present then it should be the same as the cached oldId.
                // _id should always be updated with the contact.
                Assert.IsTrue(null == _nativeContact
                    || (ContactUtil.GetID(_nativeContact, out debugId).Succeeded()
                        && debugId.Equals(_id)));
                return _id;
            }
        }

        /// <summary>
        /// Get the type of this Contact.
        /// </summary>
        public ContactType ContactType
        {
            get
            {
                _Validate();
                return _type;
            }
        }

        /// <summary>
        /// Get whether the file that backs this contact is readonly.
        /// </summary>
        /// <remarks>
        /// If this contact is not backed by a file then it is not considered readonly.
        /// </remarks>
        public bool IsReadOnly
        {
            get
            {
                _Validate();
                return !(null == _path || 0 == (File.GetAttributes(_path) & FileAttributes.ReadOnly));
            }
        }

        /// <summary>
        /// Get the path of the file that backs this contact.
        /// </summary>
        /// <remarks>
        /// Not all contacts are backed by files.
        /// If this contact is not backed by a path then the property is null.
        /// </remarks>
        public string Path
        {
            get
            {
                // Don't call validate.  Allow callers to still retrieve the Path when the state is inconsistent.

                string debugPath;
                // If the native contact is present then it should be the same as the cached path.
                // _path should always be updated with the contact.
                Assert.IsTrue(null == _path
                    || null == _nativeContact
                    || (ContactUtil.GetPath(_nativeContact, out debugPath).Succeeded()
                        && debugPath.Equals(_path)));
                return _path;
            }
        }

        public Stream GetBinaryProperty(string propertyName, StringBuilder propertyType)
        {
            _Validate();
            Validate.NotNull(propertyName, "propertyName");

            Stream stm;
            string type;
            HRESULT hr = ContactUtil.GetBinary((IContactProperties)_nativeContact, propertyName, true, out type, out stm);
            // If there's not a value at this property, just return null rather than throw
            // an exception.
            if ((HRESULT)Win32Error.ERROR_PATH_NOT_FOUND == hr)
            {
                return null;
            }
            // All errors other than a missing path warrant an exception.
            hr.ThrowIfFailed("Unable to get the binary value for " + propertyName + ".");

            if (null != propertyType)
            {
                propertyType.Append(type);
            }
            return stm;
        }

        public DateTime? GetDateProperty(string propertyName)
        {
            _Validate();
            Validate.NotNull(propertyName, "propertyName");

            DateTime dt;
            HRESULT hr = ContactUtil.GetDate((IContactProperties)_nativeContact, propertyName, true, out dt);
            // If there's not a value at this property, just return null rather than throw
            // an exception.
            if ((HRESULT)Win32Error.ERROR_PATH_NOT_FOUND == hr)
            {
                return null;
            }
            // All errors other than a missing path warrant an exception.
            hr.ThrowIfFailed("Unable to get the date value for " + propertyName + ".");

            return dt;
        }

        /// <summary>
        /// Get the collection of labels for a given node.
        /// </summary>
        /// <param name="nodeName">
        /// The node that contains the labels.
        /// </param>
        /// <returns>
        /// The collection of labels for the node.
        /// </returns>
        public ILabelCollection GetLabelCollection(string nodeName)
        {
            _Validate();
            Validate.NotNull(nodeName, "nodeName");
            return new LabelCollection(this, nodeName);
        }

        /// <summary>
        /// Get an enumerator over properties in the contact.
        /// </summary>
        /// <returns>
        /// An enumerator over all properties in the contact.
        /// </returns>
        /// <remarks>
        /// This is semantically equivalent to calling
        /// GetPropertyCollection(null, null, false).
        /// </remarks>
        public IEnumerable<ContactProperty> GetPropertyCollection()
        {
            return GetPropertyCollection(null, null, false);
        }

        /// <summary>
        /// Get an enumerator over properties in the contact.
        /// </summary>
        /// <param name="collectionName">
        /// The collection of properties to enumerate.  Optional.
        /// If this is null then all properties are returned in the enumerator.
        /// </param>
        /// <param name="labelFilter">
        /// The labels to use as a filter.  Optional.
        /// If this is not null then only array nodes are returned in the enumeration.
        /// </param>
        /// <param name="anyLabelMatches">
        /// If label filters are provided, whether the filter means any
        /// label matches or all labels must match.
        /// </param>
        /// <returns>
        /// An enumerator over properties in the contact.
        /// </returns>
        public IEnumerable<ContactProperty> GetPropertyCollection(string collectionName, string[] labelFilter, bool anyLabelMatches)
        {
            // This makes a copy of the contact and uses that for the property enumeration
            // because a bug in Windows causes contacts that have been modified in memory to
            // not properly walk simple extensions.  This is mitigated by saving the contact
            // to a stream and reloading it there.
            // CONSIDER: tracking whether the contact has been modified and only create this
            // copy when necessary.  Right now streamlining the implementation, if perf turns
            // intoa problem because of this then revisit the decision.

            _Validate();
            ContactRcw copyContact = null;
            IContactPropertyCollection propertyCollection = null;
            try
            {
                using (MemoryStream memstm = new MemoryStream())
                {
                    using (ManagedIStream pstm = new ManagedIStream(memstm))
                    {
                        ((IPersistStream)_nativeContact).Save(pstm, true);
                        
                        // Reset the seek pointer.
                        pstm.Seek(0, (int)SeekOrigin.Begin, IntPtr.Zero);

                        copyContact = new ContactRcw();
                        copyContact.Load(pstm)
                            .ThrowIfFailed("Unable to load the copy of the contact.");

                        ContactUtil.GetPropertyCollection(copyContact, collectionName, labelFilter, anyLabelMatches, out propertyCollection)
                            .ThrowIfFailed("Unable to create the propertyName enumerator.");

                        while (true)
                        {
                            _Validate();
                            HRESULT hr = propertyCollection.Next();
                            if (HRESULT.S_OK != hr)
                            {
                                hr.ThrowIfFailed("An error occurred while enumerating the contact's properties");
                                break;
                            }

                            yield return new ContactProperty((IContactProperties)_nativeContact, propertyCollection);
                        }
                    }
                }
            }
            finally
            {
                Utility.SafeRelease(ref propertyCollection);
                Utility.SafeRelease(ref copyContact);
            }
        }

        public string GetStringProperty(string propertyName)
        {
            _Validate();
            Validate.NotNull(propertyName, "propertyName");

            string s;
            HRESULT hr = ContactUtil.GetString((IContactProperties)_nativeContact, propertyName, true, out s);
            // If there's not a string at this property, just return null rather than throw
            // an exception.
            if ((HRESULT)Win32Error.ERROR_PATH_NOT_FOUND == hr)
            {
                return "";
            }
            Assert.IsFalse(string.IsNullOrEmpty(s));
            // All errors other than a missing path warrant an exception.
            hr.ThrowIfFailed("Unable to get the string for " + propertyName + ".");

            return s;
        }

        public void RemoveNode(string nodeName)
        {
            _Validate();
            Validate.NotNull(nodeName, "nodeName");

            HRESULT hr = ContactUtil.DeleteArrayNode((IContactProperties)_nativeContact, nodeName);
            // Don't really care if the path is missing, we were removing it anyways.
            // Though we don't actually want to send a notification in that case.
            if ((HRESULT)Win32Error.ERROR_PATH_NOT_FOUND != hr)
            {
                hr.ThrowIfFailed("Unable to delete the array arrayNode " + nodeName + ".");

                // Notify listeners that this node is gone.
                NotifyPropertyChanged(this, new ContactPropertyChangedEventArgs(nodeName, ContactPropertyChangeType.NodeRemoved));
            }
        }

        public void RemoveProperty(string propertyName)
        {
            _Validate();
            Validate.NotNull(propertyName, "propertyName");

            HRESULT hr = ContactUtil.DeleteProperty((IContactProperties)_nativeContact, propertyName);
            // Don't really care if the path is missing, we were removing it anyways.
            // Though we don't actually want to send a notification in that case.
            if ((HRESULT)Win32Error.ERROR_PATH_NOT_FOUND != hr)
            {
                hr.ThrowIfFailed("Unable to remove the propertyName " + propertyName + ".");

                // Notify listeners that this property is gone.
                NotifyPropertyChanged(this, new ContactPropertyChangedEventArgs(propertyName, ContactPropertyChangeType.PropertyRemoved));
            }
        }

        public void Save(string filePath)
        {
            _Validate();
            using (FileStream fstream = new FileStream(filePath, FileMode.OpenOrCreate, FileAccess.ReadWrite, FileShare.Read))
            {
                Save(fstream);
            }
        }

        public void Save(Stream stream)
        {
            _Validate();
            using (ManagedIStream istream = new ManagedIStream(stream))
            {
                ((IPersistStream)_nativeContact).Save(istream, true);
            }

        }

        public void SetStringProperty(string propertyName, string value)
        {
            _Validate();
            Validate.NotNull(propertyName, "propertyName");

            // SetString doesn't support setting to an empty string.
            if (string.IsNullOrEmpty(value))
            {
                throw new FormatException("Value cannot be null or empty.");
            }

            if (ContactUtil.IsPropertyValidNode(propertyName))
            {
                throw new SchemaException("The string value can't be set on the property because it appears to be an array node.");
            }

            HRESULT hr = ContactUtil.SetString((IContactProperties)_nativeContact, propertyName, value);
            if ((HRESULT)Win32Error.ERROR_INVALID_DATATYPE == hr)
            {
                throw new SchemaException("Unable to set the string value for " + propertyName + "because of schema.");
            }
            hr.ThrowIfFailed("Unable to set the string value for " + propertyName + ".");

            // Notify listeners of a change.
            if (null != PropertyChanged)
            {
                PropertyChanged(this, new ContactPropertyChangedEventArgs(propertyName, ContactPropertyChangeType.PropertySet));
            }
        }

        public void SetDateProperty(string propertyName, DateTime value)
        {
            _Validate();
            Validate.NotNull(propertyName, "propertyName");

            if (ContactUtil.IsPropertyValidNode(propertyName))
            {
                throw new SchemaException("The date value can't be set on the property because it appears to be an array node.");
            }

            ContactUtil.SetDate((IContactProperties)_nativeContact, propertyName, value)
                .ThrowIfFailed("Unable to set the string value for " + propertyName + ".");

            // Notify listeners of a change.
            NotifyPropertyChanged(this, new ContactPropertyChangedEventArgs(propertyName, ContactPropertyChangeType.PropertySet));
        }

        public void SetBinaryProperty(string propertyName, Stream value, string valueType)
        {
            _Validate();
            Validate.NotNull(propertyName, "propertyName");
            Validate.NotNull(value, "value");

            if (ContactUtil.IsPropertyValidNode(propertyName))
            {
                throw new SchemaException("The binary value can't be set on the property because it appears to be an array node.");
            }

            if (!value.CanRead || !value.CanSeek || 0 >= value.Length)
            {
                throw new ArgumentException("The provided stream doesn't satisfy the necessary requirements to be stored in the contact.  It must be readable, seekable, and not empty.");
            }

            // content of valueType isn't validated.
            // ContactUtil.SetBinary doesn't wrap null valueTypes,
            // it will return E_INVALIDARG if passed NULL, so pass _something_.
            ContactUtil.SetBinary((IContactProperties)_nativeContact, propertyName, string.IsNullOrEmpty(valueType) ? "binary" : valueType, value)
                .ThrowIfFailed("Unable to set the binary value for " + propertyName + ".");

            // Notify listeners of a change.
            NotifyPropertyChanged(this, new ContactPropertyChangedEventArgs(propertyName, ContactPropertyChangeType.PropertySet));
        }

        public UserTile UserTile
        {
            get
            {
                return new UserTile(this);
            }
        }

        #endregion

        #region Named Contact Property Utilities
        // This region is all syntactic sugar.  In the COM APIs this functionality is accomplished
        // through CreateArrayNode/[Get|Set][String|Binary|Date] functions.
        // These properties give strongly typed, intellisense supported access to the common Contact properties.

        /// <summary>
        /// Get or set the Gender of this contact.
        /// </summary>
        /// <exception cref="SchemaException">
        /// Throws if there's an attempt to set the Gender to an invalid value.
        /// </exception>
        /// <remarks>
        /// If there is no Gender associated with this contact, Gender.Unspecified is returned upon query.
        /// </remarks>
        public Gender Gender
        {
            get
            {
                _Validate();
                string s = GetStringProperty(PropertyNames.Gender);
                if (string.IsNullOrEmpty(s))
                {
                    return Gender.Unspecified;
                }
                else if (s.Equals(Gender.Male.ToString()))
                {
                    return Gender.Male;
                }
                else if (s.Equals(Gender.Female.ToString()))
                {
                    return Gender.Female;
                }
                Assert.AreEqual(s, Gender.Unspecified.ToString());
                return Gender.Unspecified;
            }
            set
            {
                _Validate();
                string s;
                switch (value)
                {
                    case Gender.Female:
                    case Gender.Male:
                    case Gender.Unspecified:
                        s = value.ToString();
                        break;
                    default:
                        throw new SchemaException("A bad gender value was provided");
                }
                SetStringProperty(PropertyNames.Gender, s);
            }
        }

        /// <summary>
        /// Get or set Notes associated with this contact.
        /// </summary>
        /// <remarks>
        /// The Notes property can be used by the user or any program to
        /// store miscellaneous, unstructured information about the contact.
        /// </remarks>
        public string Notes
        {
            get
            {
                _Validate();
                return GetStringProperty(PropertyNames.Notes);
            }
            set
            {
                _Validate();
                _SetOrRemoveStringProperty(PropertyNames.Notes, value);
            }
        }

        /// <summary>
        /// Get the DateTime when this contact was created.
        /// </summary>
        public DateTime CreationDate
        {
            get
            {
                _Validate();
                // This should never fail with a null DateTime? instance.
                return (DateTime)GetDateProperty(PropertyNames.CreationDate);
            }
        }

        /// <summary>
        /// Get or set the e-mail program associated with this contact.
        /// </summary>
        /// <remarks>
        /// This information can provide assistance to a correspondent regarding the type of
        /// data representation which can be used, and how they can be packaged. This property
        /// is based on the private MIME type X-Mailer that is generally implemented by MIME user
        /// agent products.
        /// </remarks>
        public string Mailer
        {
            get
            {
                _Validate();
                return GetStringProperty(PropertyNames.Mailer);
            }
            set
            {
                _Validate();
                _SetOrRemoveStringProperty(PropertyNames.Mailer, value);
            }
        }

        /// <summary>Commit a name in the contact's collection of names.</summary>
        /// <param name="contact">The contact that is having this name committed.</param>
        /// <param name="arrayNode">The array node where this name is being committed.</param>
        /// <param name="value">The name being committed.</param>
        private static void _CommitName(Contact contact, string arrayNode, Name value)
        {
            Assert.IsTrue(arrayNode.StartsWith(PropertyNames.NameCollection + PropertyNames.NameArrayNode));
            contact._SetOrRemoveStringProperty(arrayNode + PropertyNames.FamilyName, value.FamilyName);
            contact._SetOrRemoveStringProperty(arrayNode + PropertyNames.FormattedName, value.FormattedName);
            contact._SetOrRemoveStringProperty(arrayNode + PropertyNames.Generation, value.Generation);
            contact._SetOrRemoveStringProperty(arrayNode + PropertyNames.GivenName, value.GivenName);
            contact._SetOrRemoveStringProperty(arrayNode + PropertyNames.MiddleName, value.MiddleName);
            contact._SetOrRemoveStringProperty(arrayNode + PropertyNames.NickName, value.NickName);
            contact._SetOrRemoveStringProperty(arrayNode + PropertyNames.Title, value.PersonalTitle);
            contact._SetOrRemoveStringProperty(arrayNode + PropertyNames.Phonetic, value.Phonetic);
            contact._SetOrRemoveStringProperty(arrayNode + PropertyNames.Prefix, value.Prefix);
            contact._SetOrRemoveStringProperty(arrayNode + PropertyNames.Suffix, value.Suffix);
        }

        private static Name _CreateName(Contact contact, string arrayNode)
        {
            Assert.IsTrue(arrayNode.StartsWith(PropertyNames.NameCollection + PropertyNames.NameArrayNode));
            string familyName = contact.GetStringProperty(arrayNode + PropertyNames.FamilyName);
            string formattedName = contact.GetStringProperty(arrayNode + PropertyNames.FormattedName);
            string generation = contact.GetStringProperty(arrayNode + PropertyNames.Generation);
            string givenName = contact.GetStringProperty(arrayNode + PropertyNames.GivenName);
            string middleName = contact.GetStringProperty(arrayNode + PropertyNames.MiddleName);
            string nickName = contact.GetStringProperty(arrayNode + PropertyNames.NickName);
            string title = contact.GetStringProperty(arrayNode + PropertyNames.Title);
            string phonetic = contact.GetStringProperty(arrayNode + PropertyNames.Phonetic);
            string prefix = contact.GetStringProperty(arrayNode + PropertyNames.Prefix);
            string suffix = contact.GetStringProperty(arrayNode + PropertyNames.Suffix);
            return new Name(formattedName, phonetic, prefix, title, givenName, middleName, familyName, generation, suffix, nickName);
        }

        /// <summary>
        /// The collection of names that this contact is known by.
        /// </summary>
        public ILabeledPropertyCollection<Name> Names
        {
            get
            {
                _Validate();
                if (null == _nameCollection)
                {
                    _nameCollection = new SchematizedLabeledPropertyCollection<Name>(this, PropertyNames.NameCollection, PropertyNames.NameArrayNode, _CreateName, _CommitName);
                }
                return _nameCollection;
            }
        }

        /// <summary>Commit a position in the contact's collection of positions.</summary>
        /// <param name="contact">The contact that is having this position committed.</param>
        /// <param name="arrayNode">The array node where this position is being committed.</param>
        /// <param name="value">The position being committed.</param>
        private static void _CommitPosition(Contact contact, string arrayNode, Position value)
        {
            Assert.IsTrue(arrayNode.StartsWith(PropertyNames.PositionCollection + PropertyNames.PositionArrayNode));
            contact._SetOrRemoveStringProperty(arrayNode + PropertyNames.Company, value.Company);
            contact._SetOrRemoveStringProperty(arrayNode + PropertyNames.Department, value.Department);
            contact._SetOrRemoveStringProperty(arrayNode + PropertyNames.JobTitle, value.JobTitle);
            contact._SetOrRemoveStringProperty(arrayNode + PropertyNames.Office, value.Office);
            contact._SetOrRemoveStringProperty(arrayNode + PropertyNames.Organization, value.Organization);
            contact._SetOrRemoveStringProperty(arrayNode + PropertyNames.Profession, value.Profession);
            contact._SetOrRemoveStringProperty(arrayNode + PropertyNames.Role, value.Role);
        }

        private static Position _CreatePosition(Contact contact, string arrayNode)
        {
            Assert.IsTrue(arrayNode.StartsWith(PropertyNames.PositionCollection + PropertyNames.PositionArrayNode));
            string company = contact.GetStringProperty(arrayNode + PropertyNames.Company);
            string department = contact.GetStringProperty(arrayNode + PropertyNames.Department);
            string jobTitle = contact.GetStringProperty(arrayNode + PropertyNames.JobTitle);
            string office = contact.GetStringProperty(arrayNode + PropertyNames.Office);
            string organization = contact.GetStringProperty(arrayNode + PropertyNames.Organization);
            string profession = contact.GetStringProperty(arrayNode + PropertyNames.Profession);
            string role = contact.GetStringProperty(arrayNode + PropertyNames.Role);
            return new Position(organization, role, company, department, office, jobTitle, profession);
        }

        /// <summary>
        /// The collection of positions held by this contact.
        /// </summary>
        public ILabeledPropertyCollection<Position> Positions
        {
            get
            {
                _Validate();
                if (null == _positionCollection)
                {
                    _positionCollection = new SchematizedLabeledPropertyCollection<Position>(this, PropertyNames.PositionCollection, PropertyNames.PositionArrayNode, _CreatePosition, _CommitPosition);
                }
                return _positionCollection;
            }
        }

        /// <summary>Commit a Contact Id in the contact's collection of Ids.</summary>
        /// <param name="contact">The contact that is having this Contact Id committed.</param>
        /// <param name="arrayNode">The array node where this Contact Id is being committed.</param>
        /// <param name="value">The Contact Id being committed.</param>
        private static void _CommitContactId(Contact contact, string arrayNode, Guid? value)
        {
            Assert.IsTrue(arrayNode.StartsWith(PropertyNames.ContactIdCollection + PropertyNames.ContactIdArrayNode));
            if (null != value)
            {
                contact.SetStringProperty(arrayNode + PropertyNames.Value, value.ToString());
            }
            else
            {
                contact.RemoveProperty(arrayNode + PropertyNames.Value);
            }
        }

        private static Guid? _CreateContactId(Contact contact, string arrayNode)
        {
            Guid? id = null;
            Assert.IsTrue(arrayNode.StartsWith(PropertyNames.ContactIdCollection + PropertyNames.ContactIdArrayNode));
            string value = contact.GetStringProperty(arrayNode + PropertyNames.Value);
            if (!string.IsNullOrEmpty(value))
            {
                id = new Guid(value);
            }
            return id;
        }

        /// <summary>
        /// The collection of unique Guid identifiers associated with this contact.
        /// </summary>
        /// <remarks>
        /// All contacts have at least one Id associated with them.  When contacts are merged,
        /// the winner absorbs the Id collection of the loser so ContactId strings that were
        /// associated with the old contact may still resolve against the merged contact.
        /// </remarks>
        [SuppressMessage("Microsoft.Design", "CA1006:DoNotNestGenericTypesInMemberSignatures", Justification="Bogus positive, Nullable<T> should be excluded from this message.")]
        public ILabeledPropertyCollection<Guid?> ContactIds
        {
            get
            {
                _Validate();
                if (null == _contactIdCollection)
                {
                    _contactIdCollection = new SchematizedLabeledPropertyCollection<Guid?>(this, PropertyNames.ContactIdCollection, PropertyNames.ContactIdArrayNode, _CreateContactId, _CommitContactId);
                }
                return _contactIdCollection;
            }
        }

        /// <summary>Commit a date in the contact's collection of dates.</summary>
        /// <param name="contact">The contact that is having this date committed.</param>
        /// <param name="arrayNode">The array node where this date is being committed.</param>
        /// <param name="value">The date being committed.</param>
        private static void _CommitDate(Contact contact, string arrayNode, DateTime? value)
        {
            Assert.IsTrue(arrayNode.StartsWith(PropertyNames.DateCollection + PropertyNames.DateArrayNode));
            contact._SetOrRemoveDateProperty(arrayNode + PropertyNames.Value, value);
        }

        private static DateTime? _CreateDate(Contact contact, string arrayNode)
        {
            Assert.IsTrue(arrayNode.StartsWith(PropertyNames.DateCollection + PropertyNames.DateArrayNode));
            return contact.GetDateProperty(arrayNode + PropertyNames.Value);
        }

        /// <summary>
        /// The collection of dates associated with this contact.
        /// The relevance of each date is identified by the labels on the node.
        /// </summary>
        [SuppressMessage("Microsoft.Design", "CA1006:DoNotNestGenericTypesInMemberSignatures", Justification = "Bogus positive, Nullable<T> should be excluded from this message.")]
        public ILabeledPropertyCollection<DateTime?> Dates
        {
            get
            {
                _Validate();
                if (null == _dateCollection)
                {
                    _dateCollection = new SchematizedLabeledPropertyCollection<DateTime?>(this, PropertyNames.DateCollection, PropertyNames.DateArrayNode, _CreateDate, _CommitDate);
                }
                return _dateCollection;
            }
        }

        /// <summary>Commit a url in the contact's collection of urls.</summary>
        /// <param name="contact">The contact that is having this url committed.</param>
        /// <param name="arrayNode">The array node where this url is being committed.</param>
        /// <param name="value">The url being committed.</param>
        private static void _CommitUrl(Contact contact, string arrayNode, Uri value)
        {
            Assert.IsTrue(arrayNode.StartsWith(PropertyNames.UrlCollection + PropertyNames.UrlArrayNode));
            string url = (null == value) ? null : value.ToString();
            contact._SetOrRemoveStringProperty(arrayNode + PropertyNames.Value, url);
        }

        // This is stricter than the contract provided by the COM Contact APIs.
        // It requires that the string in the contact is really a valid URI.  If it's not then
        // this will return a dummy http: string.
        private static Uri _CreateUrl(Contact contact, string arrayNode)
        {
            Assert.IsTrue(arrayNode.StartsWith(PropertyNames.UrlCollection + PropertyNames.UrlArrayNode));
            string value = contact.GetStringProperty(arrayNode + PropertyNames.Value);
            if (string.IsNullOrEmpty(value))
            {
                return null;
            }
            Uri url;
            if (Uri.TryCreate(value, UriKind.RelativeOrAbsolute, out url))
            {
                return url;
            }
            return new Uri(_stockInvalidUrl);
        }

        // Consider that this isn't a good idea.
        // The Contact isn't smart enough to actually restrict strings to URIs...
        // It's nice to enforce here, but it can make it so some values can't be read.
        /// <summary>
        /// The collection of Urls associated with this contact.
        /// </summary>
        /// <remarks>
        /// Note that the APIs for storing Urls is more strict than the underlying schema requires.
        /// Some programs may place strings in the collection which are not valid Urls, but the contact
        /// is still recognized as valid.  Such strings will not be returned by the Urls
        /// ILabeledPropertyCollection, instead returning a string such as "http://invalid_url_in_contact".<para/>
        /// These strings can still be retrieved through this API by the GetStringProperty method.
        /// </remarks>
        public ILabeledPropertyCollection<Uri> Urls
        {
            get
            {
                _Validate();
                if (null == _urlCollection)
                {
                    _urlCollection = new SchematizedLabeledPropertyCollection<Uri>(this, PropertyNames.UrlCollection, PropertyNames.UrlArrayNode, _CreateUrl, _CommitUrl);
                }
                return _urlCollection;
            }
        }

        /// <summary>Commit an instant message struct in the contact's collection of instant messages.</summary>
        /// <param name="contact">The contact that is having this IM struct committed.</param>
        /// <param name="arrayNode">The array node where this IM struct is being committed.</param>
        /// <param name="value">The IM struct being committed.</param>
        private void _CommitIM(Contact contact, string arrayNode, IMAddress value)
        {
            Assert.IsTrue(arrayNode.StartsWith(PropertyNames.IMAddressCollection + PropertyNames.IMAddressArrayNode));
            contact._SetOrRemoveStringProperty(arrayNode + PropertyNames.Value, value.Handle);
            contact._SetOrRemoveStringProperty(arrayNode + PropertyNames.Protocol , value.Protocol);
        }

        private IMAddress _CreateIM(Contact contact, string arrayNode)
        {
            Assert.IsTrue(arrayNode.StartsWith(PropertyNames.IMAddressCollection + PropertyNames.IMAddressArrayNode));
            string handle = contact.GetStringProperty(arrayNode + PropertyNames.Value);
            string protocol = contact.GetStringProperty(arrayNode + PropertyNames.Protocol);
            return new IMAddress(handle, protocol);
        }

        /// <summary>
        /// The collection of instant message addresses associated with this contact.
        /// </summary>
        public ILabeledPropertyCollection<IMAddress> IMAddresses
        {
            get
            {
                _Validate();
                if (null == _imCollection)
                {
                    _imCollection = new SchematizedLabeledPropertyCollection<IMAddress>(this, PropertyNames.IMAddressCollection, PropertyNames.IMAddressArrayNode, _CreateIM, _CommitIM);
                }
                return _imCollection;
            }
        }

        /// <summary>Commit an EmailAddress in the contact's collection of e-mail addresses.</summary>
        /// <param name="contact">The contact that is having this e-mail committed.</param>
        /// <param name="arrayNode">The array node where this e-mail is being committed.</param>
        /// <param name="value">The e-mail address being committed.</param>
        private void _CommitEmail(Contact contact, string arrayNode, EmailAddress value)
        {
            Assert.IsTrue(arrayNode.StartsWith(PropertyNames.EmailAddressCollection + PropertyNames.EmailAddressArrayNode));
            contact._SetOrRemoveStringProperty(arrayNode + PropertyNames.Address, value.Address);
            contact._SetOrRemoveStringProperty(arrayNode + PropertyNames.Type, value.Type);
        }

        private EmailAddress _CreateEmail(Contact contact, string arrayNode)
        {
            Assert.IsTrue(arrayNode.StartsWith(PropertyNames.EmailAddressCollection + PropertyNames.EmailAddressArrayNode));
            string address = contact.GetStringProperty(arrayNode + PropertyNames.Address);
            string type = contact.GetStringProperty(arrayNode + PropertyNames.Type);
            return new EmailAddress(address, type);
        }

        /// <summary>
        /// The collection of e-mail addresses associated with this contact.
        /// </summary>
        public ILabeledPropertyCollection<EmailAddress> EmailAddresses
        {
            get
            {
                _Validate();
                if (null == _emailCollection)
                {
                    _emailCollection = new SchematizedLabeledPropertyCollection<EmailAddress>(this, PropertyNames.EmailAddressCollection, PropertyNames.EmailAddressArrayNode, _CreateEmail, _CommitEmail);
                }
                return _emailCollection;
            }
        }

        /// <summary>Commit a person in the contact's collection of people.</summary>
        /// <param name="contact">The contact that is having this person committed.</param>
        /// <param name="arrayNode">The array node where this person is being committed.</param>
        /// <param name="value">The person being committed.</param>
        private void _CommitPerson(Contact contact, string arrayNode, Person value)
        {
            Assert.IsTrue(arrayNode.StartsWith(PropertyNames.PersonCollection + PropertyNames.PersonArrayNode));
            contact._SetOrRemoveStringProperty(arrayNode + PropertyNames.FormattedName, value.FormattedName);
            contact._SetOrRemoveStringProperty(arrayNode + PropertyNames.PersonId, value.Id);
        }

        private Person _CreatePerson(Contact contact, string arrayNode)
        {
            Assert.IsTrue(arrayNode.StartsWith(PropertyNames.PersonCollection + PropertyNames.PersonArrayNode));
            string name = contact.GetStringProperty(arrayNode + PropertyNames.FormattedName);
            string id = contact.GetStringProperty(arrayNode + PropertyNames.PersonId);
            return new Person(name, id, _manager);
        }

        /// <summary>
        /// The collection of people associated with this contact.
        /// The relationship of each person to this contact is identified by the labels on the node.
        /// </summary>
        public ILabeledPropertyCollection<Person> People
        {
            get
            {
                _Validate();
                if (null == _personCollection)
                {
                    _personCollection = new SchematizedLabeledPropertyCollection<Person>(this, PropertyNames.PersonCollection, PropertyNames.PersonArrayNode, _CreatePerson, _CommitPerson);
                }
                return _personCollection;
            }
        }

        /// <summary>Commit a certificate in the contact's collection of certificates.</summary>
        /// <param name="contact">The contact that is having this certificate committed.</param>
        /// <param name="arrayNode">The array node where this certificate is being committed.</param>
        /// <param name="value">The certificate being committed.</param>
        private void _CommitCertificate(Contact contact, string arrayNode, Certificate value)
        {
            Assert.IsTrue(arrayNode.StartsWith(PropertyNames.CertificateCollection + PropertyNames.CertificateArrayNode));
            contact._SetOrRemoveBinaryProperty(arrayNode + PropertyNames.Value, value.Value, value.Type);
            contact._SetOrRemoveBinaryProperty(arrayNode + PropertyNames.ThumbPrint, value.ThumbPrint, value.ThumbPrintType);
        }

        private Certificate _CreateCertificate(Contact contact, string arrayNode)
        {
            Assert.IsTrue(arrayNode.StartsWith(PropertyNames.CertificateCollection + PropertyNames.CertificateArrayNode));
            StringBuilder sbType = new StringBuilder();
            Stream value = contact.GetBinaryProperty(arrayNode + PropertyNames.Value, sbType);
            StringBuilder sbThumbPrintType = new StringBuilder();
            Stream thumbprint = contact.GetBinaryProperty(arrayNode + PropertyNames.ThumbPrint, sbThumbPrintType);
            return new Certificate(value, sbType.ToString(), thumbprint, sbThumbPrintType.ToString());
        }

        /// <summary>
        /// The collection of certificates associated with this contact.
        /// </summary>
        public ILabeledPropertyCollection<Certificate> Certificates
        {
            get
            {
                _Validate();
                if (null == _certCollection)
                {
                    _certCollection = new SchematizedLabeledPropertyCollection<Certificate>(this, PropertyNames.CertificateCollection, PropertyNames.CertificateArrayNode, _CreateCertificate, _CommitCertificate);
                }
                return _certCollection;
            }
        }

        /// <summary>Commit a physical address in the contact's collection of addresses.</summary>
        /// <param name="contact">The contact that is having this address committed.</param>
        /// <param name="arrayNode">The array node where this address is being committed.</param>
        /// <param name="value">The address being committed.</param>
        private void _CommitAddress(Contact contact, string arrayNode, PhysicalAddress value)
        {
            Assert.IsTrue(arrayNode.StartsWith(PropertyNames.PhysicalAddressCollection + PropertyNames.PhysicalAddressArrayNode));
            contact._SetOrRemoveStringProperty(arrayNode + PropertyNames.AddressLabel, value.AddressLabel);
            contact._SetOrRemoveStringProperty(arrayNode + PropertyNames.Locality, value.City);
            contact._SetOrRemoveStringProperty(arrayNode + PropertyNames.Country, value.Country);
            contact._SetOrRemoveStringProperty(arrayNode + PropertyNames.ExtendedAddress, value.ExtendedAddress);
            contact._SetOrRemoveStringProperty(arrayNode + PropertyNames.POBox, value.POBox);
            contact._SetOrRemoveStringProperty(arrayNode + PropertyNames.Region, value.State);
            contact._SetOrRemoveStringProperty(arrayNode + PropertyNames.Street, value.Street);
            contact._SetOrRemoveStringProperty(arrayNode + PropertyNames.PostalCode, value.ZipCode);
        }

        private PhysicalAddress _CreateAddress(Contact contact, string arrayNode)
        {
            Assert.IsTrue(arrayNode.StartsWith(PropertyNames.PhysicalAddressCollection + PropertyNames.PhysicalAddressArrayNode));
            string label = contact.GetStringProperty(arrayNode + PropertyNames.AddressLabel);
            string city = contact.GetStringProperty(arrayNode + PropertyNames.Locality);
            string country = contact.GetStringProperty(arrayNode + PropertyNames.Country);
            string extended = contact.GetStringProperty(arrayNode + PropertyNames.ExtendedAddress);
            string pobox = contact.GetStringProperty(arrayNode + PropertyNames.POBox);
            string state = contact.GetStringProperty(arrayNode + PropertyNames.Region);
            string street = contact.GetStringProperty(arrayNode + PropertyNames.Street);
            string zip = contact.GetStringProperty(arrayNode + PropertyNames.PostalCode);
            return new PhysicalAddress(pobox, street, city, state, zip, country, extended, label);
        }

        /// <summary>
        /// The collection of physical addresses associated with this contact.
        /// </summary>
        public ILabeledPropertyCollection<PhysicalAddress> Addresses
        {
            get
            {
                _Validate();
                if (null == _addressCollection)
                {
                    _addressCollection = new SchematizedLabeledPropertyCollection<PhysicalAddress>(this, PropertyNames.PhysicalAddressCollection, PropertyNames.PhysicalAddressArrayNode, _CreateAddress, _CommitAddress);
                }
                return _addressCollection;
            }
        }

        /// <summary>Commit a phone number in the contact's collection of numbers.</summary>
        /// <param name="contact">The contact that is having this number committed.</param>
        /// <param name="arrayNode">The array node where this number is being committed.</param>
        /// <param name="value">The number being committed.</param>
        private void _CommitNumber(Contact contact, string arrayNode, PhoneNumber value)
        {
            Assert.IsTrue(arrayNode.StartsWith(PropertyNames.PhoneNumberCollection + PropertyNames.PhoneNumberArrayNode));
            contact._SetOrRemoveStringProperty(arrayNode + PropertyNames.Alternate, value.Alternate);
            contact._SetOrRemoveStringProperty(arrayNode + PropertyNames.Number, value.Number);
        }

        private PhoneNumber _CreateNumber(Contact contact, string arrayNode)
        {
            Assert.IsTrue(arrayNode.StartsWith(PropertyNames.PhoneNumberCollection + PropertyNames.PhoneNumberArrayNode));
            string number = contact.GetStringProperty(arrayNode + PropertyNames.Number);
            string alternate = contact.GetStringProperty(arrayNode + PropertyNames.Alternate);
            return new PhoneNumber(number, alternate);
        }

        /// <summary>
        /// The collection of phone numbers associated with this contact.
        /// </summary>
        public ILabeledPropertyCollection<PhoneNumber> PhoneNumbers
        {
            get
            {
                _Validate();
                if (null == _numberCollection)
                {
                    _numberCollection = new SchematizedLabeledPropertyCollection<PhoneNumber>(this, PropertyNames.PhoneNumberCollection, PropertyNames.PhoneNumberArrayNode, _CreateNumber, _CommitNumber);
                }
                return _numberCollection;
            }
        }

        /// <summary>Commit a photo struct in the contact's collection of photos.</summary>
        /// <param name="contact">The contact that is having this photo committed.</param>
        /// <param name="arrayNode">The array node where this photo is being committed.</param>
        /// <param name="value">The photo being committed.</param>
        private void _CommitPhoto(Contact contact, string arrayNode, Photo value)
        {
            Assert.IsTrue(arrayNode.StartsWith(PropertyNames.PhotoCollection + PropertyNames.PhotoArrayNode));
            contact._SetOrRemoveBinaryProperty(arrayNode + PropertyNames.Value, value.Value, value.ValueType);
            string url = null;
            if (null != value.Url)
            {
                url = value.Url.ToString();
            }
            contact._SetOrRemoveStringProperty(arrayNode + PropertyNames.Url, url);
        }

        private Photo _CreatePhoto(Contact contact, string arrayNode)
        {
            Assert.IsTrue(arrayNode.StartsWith(PropertyNames.PhotoCollection + PropertyNames.PhotoArrayNode));
            Uri url = null;
            string urlString = contact.GetStringProperty(arrayNode + PropertyNames.Url);
            if (!string.IsNullOrEmpty(urlString))
            {
                if (!Uri.TryCreate(urlString, UriKind.RelativeOrAbsolute, out url))
                {
                    url = new Uri(_stockInvalidUrl);
                }
            }
            StringBuilder photoType = new StringBuilder();
            Stream photoStream = contact.GetBinaryProperty(arrayNode + PropertyNames.Value, photoType);
            return new Photo(photoStream, null == photoStream ? null : photoType.ToString(), url);
        }

        /// <summary>
        /// The collection of photos associated with this contact.
        /// </summary>
        /// <remarks>
        /// Photos generally consist of either a data-stream with an associated MIME type, or a url
        /// with a location where the image is stored.  For when the Url is used, the APIs for storing
        /// Urls is more strict than the underlying schema requires.  Some programs may place strings
        /// in the collection which are not valid Urls, but the contact is still recognized as valid.
        /// Such strings will not be returned by the Urls ILabeledPropertyCollection, instead returning
        /// a string such as "http://invalid_url_in_contact".  Also for security reasons, some programs
        /// may ignore urls that do not represent a location the local file system.<para/>
        /// These strings can still be retrieved through this API by the GetStringProperty method.
        /// </remarks>
        public ILabeledPropertyCollection<Photo> Photos
        {
            get
            {
                _Validate();
                if (null == _photoCollection)
                {
                    _photoCollection = new SchematizedLabeledPropertyCollection<Photo>(this, PropertyNames.PhotoCollection, PropertyNames.PhotoArrayNode, _CreatePhoto, _CommitPhoto);
                }
                return _photoCollection;
            }
        }
        #endregion

        #region IDisposable Pattern

        /// <summary>
        /// Release all resources held by this object.
        /// </summary>
        /// <remarks>
        /// Dispose can safely be called multiple times, but the object cannot be used for anything else.
        /// </remarks>
        public void Dispose()
        {
            Dispose(true);
            GC.SuppressFinalize(this);
        }

        /// <summary>
        /// Finalize the object.
        /// </summary>
        ~Contact()
        {
            Dispose(false);
        }

        /// <summary>
        /// Protected overload of Dispose that is standard in IDisposable patterns.
        /// </summary>
        /// <param name="disposing">Whether or not this is being called by Dispose, rather than by the finalizer.</param>
        /// <remarks>
        /// Overrides of this method should always call base.Dispose.
        /// </remarks>
        protected virtual void Dispose(bool disposing)
        {
            if (disposing)
            {
                // Don't do this from the finalizer's thread.
                Utility.SafeRelease(ref _nativeContact);
            }
        }

        #endregion

        #region INotifyPropertyChanged Members

        public event PropertyChangedEventHandler PropertyChanged;

        #endregion
    }
}
